"""
Microsoft Outlook邮件读取工具

此脚本用于通过OAuth2认证连接Microsoft Outlook邮箱，并读取邮件内容。
它支持IMAP和POP3两种协议连接邮箱。

使用前需要:
1. 在Microsoft Azure门户注册应用并获取client_id
2. 获取有效的refresh_token
3. 确保应用有适当的权限 (IMAP.AccessAsUser.All, POP.AccessAsUser.All)

作者: [作者名称]
日期: [创建日期]
"""

import base64
import email
import email.header
import requests
import imaplib
import poplib
import sys
import argparse
import os
import re
from getpass import getpass


# 1. 使用refresh_token获取access_token
def get_accesstoken(client_id, refresh_token):
    """
    使用refresh_token从Microsoft OAuth2服务获取access_token
    
    :param client_id: Microsoft Azure应用的客户端ID
    :param refresh_token: 用于获取新access_token的刷新令牌
    :return: 访问令牌(access_token)
    :raises: Exception 如果获取令牌失败
    """
    data = {
        'client_id': client_id,
        'grant_type': 'refresh_token',
        'refresh_token': refresh_token
    }
    
    try:
        ret = requests.post('https://login.microsoftonline.com/consumers/oauth2/v2.0/token', data=data)
        ret.raise_for_status()  # 如果HTTP请求返回错误状态码，抛出异常
        
        print(ret.text)
        
        # 添加错误处理
        response_json = ret.json()
        if 'error' in response_json:
            error_msg = response_json.get('error_description', response_json.get('error', '未知错误'))
            raise Exception(f"获取访问令牌失败: {error_msg}")
        
        if 'access_token' not in response_json:
            raise Exception("响应中没有访问令牌")
            
        return response_json['access_token']
    except requests.exceptions.RequestException as e:
        raise Exception(f"HTTP请求失败: {e}")


def generate_auth_string(user, token):
    """
    生成XOAUTH2认证字符串
    
    格式遵循RFC 7628 (https://tools.ietf.org/html/rfc7628)
    
    :param user: 邮箱地址
    :param token: OAuth2访问令牌
    :return: 格式化的认证字符串
    """
    auth_string = f"user={user}\1auth=Bearer {token}\1\1"
    return auth_string


def decode_unicode_escape(text):
    """
    解码字符串中的Unicode转义序列
    
    :param text: 包含Unicode转义序列的字符串
    :return: 解码后的字符串
    """
    # 使用正则表达式查找形如 \uXXXX 的Unicode转义序列
    pattern = r'\\u([0-9a-fA-F]{4})'
    
    # 定义替换函数
    def replace_unicode(match):
        # 将十六进制转换为整数，然后转换为对应的Unicode字符
        return chr(int(match.group(1), 16))
    
    # 替换所有匹配项
    return re.sub(pattern, replace_unicode, text)


def tuple_to_str(tuple_):
    """
    将邮件头部解码后的元组转换为字符串
    
    :param tuple_: 转换前的元组，格式可能为:
                  - (b'\xcd\xf5\xd4\xc6', 'gbk') - 带编码信息的二进制数据
                  - (b' <XXXX@163.com>', None) - 无编码信息的二进制数据
                  - ('<XXXX@163.com>', None) - 字符串数据
    :return: 转换后的字符串
    """
    try:
        if tuple_[1]:
            # 如果提供了编码，使用该编码解码
            out_str = tuple_[0].decode(tuple_[1])
        else:
            # 没有提供编码
            if isinstance(tuple_[0], bytes):
                # 尝试使用不同的编码解码
                encodings = ['utf-8', 'gbk', 'latin-1']
                for encoding in encodings:
                    try:
                        out_str = tuple_[0].decode(encoding)
                        break
                    except UnicodeDecodeError:
                        if encoding == encodings[-1]:  # 如果是最后一个尝试的编码
                            # 使用latin-1作为最后的尝试，它可以解码任何字节序列
                            out_str = tuple_[0].decode('latin-1')
            else:
                out_str = tuple_[0]
        return out_str
    except Exception as e:
        print(f"解码邮件头部时出错: {e}")
        # 返回一个安全的默认值
        return str(tuple_)


def fetch_email_body(mail, item):
    """
    获取并处理邮件内容
    :param mail: 邮件连接对象
    :param item: 邮件ID
    """
    try:
        print(f"正在获取邮件 ID: {item.decode('utf-8') if isinstance(item, bytes) else item}")
        ret, data = mail.fetch(item, '(RFC822)')
        
        if ret != 'OK' or not data or not data[0]:
            print(f"获取邮件失败: {ret}")
            return
            
        try:
            # 尝试使用UTF-8解码
            msg_data = data[0][1].decode('utf-8')
        except UnicodeDecodeError:
            # 如果UTF-8解码失败，尝试使用Latin-1
            print("UTF-8解码失败，尝试使用Latin-1")
            msg_data = data[0][1].decode('latin-1')
            
        msg = email.message_from_string(msg_data)
        
        # 获取发件人
        from_addr = msg.get('from', 'Unknown')
        print(f"发件人: {from_addr}")
        
        # 获取日期
        date = msg.get('date', 'Unknown')
        print(f"日期: {date}")
        
        # 获取并解码主题
        sub = msg.get('subject', '')
        sub_text = email.header.decode_header(str(sub))
        
        # 主题
        sub_detail = ''
        if sub_text[0]:
            sub_detail = tuple_to_str(sub_text[0])
        print(f"主题: {sub_detail}")
        
        # 处理邮件内容
        for part in msg.walk():
            if not part.is_multipart():
                content_type = part.get_content_type()
                name = part.get_filename()
                
                if not name:
                    try:
                        payload = part.get_payload(decode=True)
                        if payload:
                            # 尝试检测编码
                            charset = part.get_content_charset() or 'utf-8'
                            try:
                                txt = payload.decode(charset)
                            except UnicodeDecodeError:
                                print(f"使用 {charset} 解码失败，尝试使用 latin-1")
                                txt = payload.decode('latin-1')
                                
                            if content_type == 'text/plain':
                                # 处理Unicode转义序列
                                decoded_txt = decode_unicode_escape(txt)
                                
                                print("邮件内容 (纯文本):")
                                print("-" * 50)
                                print(decoded_txt[:1000] + "..." if len(decoded_txt) > 1000 else decoded_txt)  # 打印前1000个字符
                                print("-" * 50)
                            # 不显示HTML内容
                    except Exception as decode_error:
                        print(f"解码邮件内容时出错: {decode_error}")
                else:
                    print(f"附件: {name}")
                    
    except Exception as e:
        print(f"处理邮件时出错: {e}")
        import traceback
        traceback.print_exc()  # 打印详细的错误堆栈

def getmail(sel, mail, only_latest=True):
    """
    获取邮箱中的邮件
    
    :param sel: 邮箱文件夹名称
    :param mail: 邮件连接对象
    :param only_latest: 是否只获取最新的邮件
    """
    try:
        mail.select(sel)
        status, messages = mail.search(None, 'ALL')  # UNSEEN 为未读邮件
        if status != 'OK':
            print(f"搜索邮件失败: {status}")
            return
            
        all_emails = messages[0].split()
        total_emails = len(all_emails)
        print(f"找到 {total_emails} 封邮件")
        
        if total_emails == 0:
            print("没有找到邮件")
            return
            
        if only_latest:
            # 只处理最新的邮件（列表中的最后一个）
            latest_email = all_emails[-1]
            print(f"正在处理最新邮件 (ID: {latest_email.decode('utf-8') if isinstance(latest_email, bytes) else latest_email})")
            fetch_email_body(mail, latest_email)
        else:
            # 处理所有邮件
            for item in all_emails:
                fetch_email_body(mail, item)
    except Exception as e:
        print(f"获取邮件时出错: {e}")


# 2. 使用访问令牌连接 IMAP
def connect_imap(emailadr, access_token, only_latest=True, folders=None):
    """
    使用OAuth2访问令牌连接IMAP服务器并获取邮件
    
    :param emailadr: 邮箱地址
    :param access_token: OAuth2访问令牌
    :param only_latest: 是否只获取最新的邮件
    :param folders: 要读取的文件夹列表，默认为['INBOX']
    """
    if folders is None:
        folders = ['INBOX']
        
    try:
        print(f"正在连接到 IMAP 服务器 (outlook.live.com)...")
        mail = imaplib.IMAP4_SSL('outlook.live.com')
        
        print(f"正在使用 XOAUTH2 进行认证...")
        mail.authenticate('XOAUTH2', lambda x: generate_auth_string(emailadr, access_token))
        
        # 获取可用文件夹列表
        status, folder_list = mail.list()
        if status == 'OK':
            print("可用文件夹:")
            for folder in folder_list:
                if folder:
                    folder_name = folder.decode('utf-8').split('"')[-2] if '"' in folder.decode('utf-8') else folder.decode('utf-8').split(' ')[-1]
                    print(f"- {folder_name}")
        
        # 读取指定文件夹
        for folder in folders:
            print(f'\n读取文件夹: {folder}')
            getmail(folder, mail, only_latest)
        
        # 关闭连接
        mail.logout()
        print("IMAP 连接已关闭")
    except imaplib.IMAP4.error as e:
        print(f"IMAP 错误: {e}")
    except Exception as e:
        print(f"连接 IMAP 服务器时出错: {e}")


def connect_pop3(emailadr, access_token, only_latest=True):
    """
    使用OAuth2访问令牌连接POP3服务器并获取邮件
    
    :param emailadr: 邮箱地址
    :param access_token: OAuth2访问令牌
    :param only_latest: 是否只获取最新的邮件
    """
    try:
        print(f"正在连接到 POP3 服务器 (outlook.live.com)...")
        server = poplib.POP3_SSL('outlook.live.com')
        
        print(f"正在使用 XOAUTH2 进行认证...")
        token = generate_auth_string(emailadr, access_token)
        encoded_auth_string = base64.b64encode(token.encode("utf-8")).decode("utf-8")
        
        resp = server._shortcmd(f'AUTH XOAUTH2')
        print(f"AUTH 命令响应: {resp}")
        
        resp = server._shortcmd(f'{encoded_auth_string}')
        print(f"认证响应: {resp}")
        
        # 获取邮件列表
        status, msg_list, size = server.list()
        print(f"邮件列表状态: {status}")
        total_emails = len(msg_list)
        print(f"找到 {total_emails} 封邮件")
        
        if total_emails > 0:
            if only_latest:
                # 只获取最新的邮件（列表中的最后一个）
                latest_email_num = total_emails
                print(f"正在获取最新邮件 (编号: {latest_email_num})")
                
                # 获取邮件内容
                resp, lines, octets = server.retr(latest_email_num)
                print(f"获取邮件响应: {resp}")
                
                # 将邮件内容连接成一个字符串
                msg_content = b'\n'.join(lines)
                
                # 解析邮件内容
                try:
                    msg = email.message_from_bytes(msg_content)
                    
                    # 获取发件人
                    from_addr = msg.get('from', 'Unknown')
                    print(f"发件人: {from_addr}")
                    
                    # 获取日期
                    date = msg.get('date', 'Unknown')
                    print(f"日期: {date}")
                    
                    # 获取并解码主题
                    sub = msg.get('subject', '')
                    sub_text = email.header.decode_header(str(sub))
                    
                    # 主题
                    sub_detail = ''
                    if sub_text[0]:
                        sub_detail = tuple_to_str(sub_text[0])
                    print(f"主题: {sub_detail}")
                    
                    # 处理邮件内容
                    for part in msg.walk():
                        if not part.is_multipart():
                            content_type = part.get_content_type()
                            name = part.get_filename()
                            
                            if not name:
                                try:
                                    payload = part.get_payload(decode=True)
                                    if payload:
                                        # 尝试检测编码
                                        charset = part.get_content_charset() or 'utf-8'
                                        try:
                                            txt = payload.decode(charset)
                                        except UnicodeDecodeError:
                                            print(f"使用 {charset} 解码失败，尝试使用 latin-1")
                                            txt = payload.decode('latin-1')
                                            
                                        if content_type == 'text/plain':
                                            # 处理Unicode转义序列
                                            decoded_txt = decode_unicode_escape(txt)
                                            
                                            print("邮件内容 (纯文本):")
                                            print("-" * 50)
                                            print(decoded_txt[:1000] + "..." if len(decoded_txt) > 1000 else decoded_txt)  # 打印前1000个字符
                                            print("-" * 50)
                                        # 不显示HTML内容
                                except Exception as decode_error:
                                    print(f"解码邮件内容时出错: {decode_error}")
                            else:
                                print(f"附件: {name}")
                except Exception as e:
                    print(f"解析邮件内容时出错: {e}")
                    import traceback
                    traceback.print_exc()  # 打印详细的错误堆栈
            else:
                print("获取所有邮件功能尚未实现，请使用IMAP协议获取所有邮件")
        
        server.quit()
        print("POP3 连接已关闭")
    except poplib.error_proto as e:
        print(f"POP3 协议错误: {e}")
    except Exception as e:
        print(f"连接 POP3 服务器时出错: {e}")


def print_help_info():
    """打印帮助信息，说明如何获取client_id和refresh_token"""
    print("\n=== 如何获取Microsoft应用凭证 ===")
    print("1. 访问 https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade")
    print("2. 点击'新注册'创建新应用")
    print("3. 设置重定向URI为 http://localhost")
    print("4. 在'API权限'中添加 IMAP.AccessAsUser.All 和 POP.AccessAsUser.All 权限")
    print("5. 记下应用程序(客户端)ID，这就是你需要的client_id")
    print("6. 获取refresh_token需要额外的步骤，可以使用MSAL库或其他OAuth2工具")
    print("   详细说明请参考: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow")
    print("===============================\n")


def parse_arguments():
    """解析命令行参数"""
    parser = argparse.ArgumentParser(description='Microsoft Outlook邮件读取工具')
    
    parser.add_argument('--client-id', 
                        help='Microsoft Azure应用的客户端ID')
    
    parser.add_argument('--refresh-token', 
                        help='用于获取新access_token的刷新令牌')
    
    parser.add_argument('--email', 
                        help='要连接的邮箱地址')
    
    parser.add_argument('--protocol', choices=['imap', 'pop3', 'both'], default='imap',
                        help='要使用的协议 (imap, pop3, both)，默认为imap')
    
    parser.add_argument('--only-latest', action='store_true', default=True,
                        help='是否只查看最新邮件（默认为True）')
    
    parser.add_argument('--all-emails', dest='only_latest', action='store_false',
                        help='查看所有邮件，而不仅仅是最新邮件')
    
    parser.add_argument('--folders', nargs='+', default=['INBOX'],
                        help='要查看的文件夹列表，默认为INBOX')
    
    parser.add_argument('--help-info', action='store_true',
                        help='显示如何获取client_id和refresh_token的帮助信息')
    
    return parser.parse_args()


# 主流程
if __name__ == '__main__':
    args = parse_arguments()
    
    # 显示帮助信息
    if args.help_info:
        print_help_info()
        sys.exit(0)
    
    try:
        # 从命令行参数、环境变量或默认值获取配置
        client_id = args.client_id or os.environ.get('MS_CLIENT_ID') or 'dbc8e03a-b00c-46bd-ae65-b683e7707cb0'
        
        # 如果没有提供refresh_token，尝试从环境变量获取，否则提示用户输入
        if args.refresh_token:
            refresh_token = args.refresh_token
        elif os.environ.get('MS_REFRESH_TOKEN'):
            refresh_token = os.environ.get('MS_REFRESH_TOKEN')
        else:
            print("请输入refresh_token (输入将不会显示在屏幕上):")
            refresh_token = getpass()
            
        # 如果没有提供email，尝试从环境变量获取，否则提示用户输入
        emailadr = args.email or os.environ.get('MS_EMAIL') or input("请输入邮箱地址: ")
        
        # 验证必要的参数
        if not client_id or client_id == 'dbc8e03a-b00c-46bd-ae65-b683e7707cb0':
            print("警告: 使用的是示例client_id，这可能无法正常工作")
            print("请提供有效的client_id (使用--client-id参数或设置MS_CLIENT_ID环境变量)")
            print_help_info()
            if input("是否继续尝试? (y/n): ").lower() != 'y':
                sys.exit(1)
                
        if not refresh_token:
            print("错误: 未提供refresh_token")
            print("请使用--refresh-token参数或设置MS_REFRESH_TOKEN环境变量")
            sys.exit(1)
            
        if not emailadr:
            print("错误: 未提供邮箱地址")
            print("请使用--email参数或设置MS_EMAIL环境变量")
            sys.exit(1)
        
        print("正在获取访问令牌...")
        access_token = get_accesstoken(client_id, refresh_token)
        print("获取的访问令牌:", access_token[:20] + "..." if len(access_token) > 20 else access_token)
        
        # 根据选择的协议连接邮箱
        if args.protocol in ['imap', 'both']:
            print('\nIMAP测试')
            connect_imap(emailadr, access_token, args.only_latest, args.folders)
        
        if args.protocol in ['pop3', 'both']:
            print('\nPOP3测试')
            connect_pop3(emailadr, access_token, args.only_latest)
        
    except Exception as e:
        print(f"错误: {e}")
        print("\n可能的解决方案:")
        print("1. 检查client_id是否正确 (应为36个字符的GUID)")
        print("2. 检查refresh_token是否有效或已过期")
        print("3. 确认应用程序已在Microsoft Azure门户中正确注册")
        print("4. 确认应用程序有适当的权限 (IMAP.AccessAsUser.All, POP.AccessAsUser.All)")
        print("5. 运行 'python ReadMail.py --help-info' 获取更多信息")
        print("6. 运行 'python ReadMail.py --help' 查看所有可用的命令行参数")